using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Serialization;   
using PigeonCoopToolkit.Effects.Trails;

public enum BodyState
{
	Normal = 0, 
	Evading,		// won't be hit by normal injury, but not immune to special injury
	Inactive, 		// won't be hit any way
	Defending,		// will be hit, take less damage and transit to defend state
	Armor,			// will be hit, take less damage but current state won't be interrupted
}

[RequireComponent(typeof(Rigidbody))]
public class Character : MonoBehaviour
{
	[System.NonSerialized] public GameObject gameObjectRef;
	[System.NonSerialized] public Transform transformRef;
	[System.NonSerialized] public Rigidbody rigidbodyRef;
	public Transform modelTransformRef;
	public Animation animationRef;
	new public Animation animation
	{
		get { return animationRef; }
	}

	public SkinnedMeshRenderer bodyRenderer;

	//[System.NonSerialized]
	public CharacterAnimatedState curState = null;

	[System.NonSerialized]
	public float curFrameTime = 0;

	public int curFrame { 
		get {return curFrame != null? curState.curFrame: 0;}  
	}

	public string characterName;
	public GameObject characterTemplate;
	public BodyState bodyState;

	#region CharacterAnimationState
	public CharacterAnimatedState[] attackStates;
	public CharacterAnimatedState defaultState; 
	public CharacterAnimatedState defendState;
	public CharacterAnimatedState defenseBrokenState;
	public CharacterAnimatedState injuredState;
	public CharacterAnimatedState fallState;
	public CharacterAnimatedState moveState;
	public CharacterAnimatedState deadState;
    public CharacterAnimatedState slashState;
	public CharacterAnimatedState[] stateList;
	public Dictionary<string, CharacterAnimatedState> stateIndex 
		= new Dictionary<string, CharacterAnimatedState>(); 
	#endregion

	[System.NonSerialized] 
	public float targetRotation;
	public Vector3 pushForce;

    public int delayFrames;

	protected bool shaking;
	protected float shakeDistance;
	protected float shakeFrequency;
	protected float freezedAnimationTime;
	protected float freezedAnimationSpeed;

    public delegate void CustomInputDelegate( );
    public CustomInputDelegate customInput = null;

	[SerializeField] 
	protected BoxCollider firstAttackCollider;

    public GameObject weaponFlashObj;

	public float attackRange;

    public Trail trail;
    public WeaponTrail[] RPGTrails;

	public CharacterProperty Property{
		get {return property;}  
	}
	private CharacterProperty property;

	public List<Character> counterCharacters = new List<Character>();

	public AnimationState currentAnimationState
	{
		get 
		{
			if(curState == null && curState.animationClip == null)
				return null;
			return animationRef[curState.animationClip.name];
		}
	}

	public float attackSpeed
	{
		set
		{
			for(int i=0;i<attackStates.Length;i++)
			{
				attackStates[i].animationSpeed = value;
			}
		}
	}

    private Character lockTarget;
    public Character LockTarget
    {
        get 
        {
            return lockTarget;
        }
        set
        {
            if(lockTarget == null || lockTarget.property.hp <= 0)//only no lockTarget or lockTarget died can set new lockTarget
            {
                lockTarget =  value;
            }
        }
    }

    public List<CharacterAnimatedState> lockedAttackStates = new List<CharacterAnimatedState>();
    public List<string> lockedAttackStatesNames = new List<string>();

	public BloodEffect bloodEffect;
    public Transform slashSign;
    public float slashRatio = 0.1f;
	public Character()
	{

	}

	void Awake()
	{
		Init();
	}
	
	protected virtual void Init()
	{   
		// check reference 
		gameObjectRef = gameObject;
		transformRef = transform;
		rigidbodyRef = rigidbody;
		property = GetComponent<CharacterProperty>();
		if(animationRef == null)
		{
			Debug.LogError(gameObjectRef.ToString() + "No Animation Ref"); 
		}

		delayFrames = 0;
		shaking = false;
		pushForce = Vector3.zero;

		IndexStates();
		for(int i=0;i<stateList.Length;i++)
		{
			stateList[i].Init(this); 
		}

        //for lock target attack
        foreach(CharacterAnimatedState state in lockedAttackStates)
        {
            lockedAttackStatesNames.Add(state.stateName);
        }

		// to default state
		ToState(defaultState.stateName);

		// default message handlers;
		onHitting.OnEvent += SendDamage;
		onDamaged.OnEvent += ReceiveDamage;

        //used for AI 
		if(firstAttackCollider != null){
			attackRange = Vector3.Distance(firstAttackCollider.transform.position, transform.position);
		}

		SetPropertyData();
	}

	public void IndexStates()
	{
		stateList = GetComponentsInChildren<CharacterAnimatedState>();
		stateIndex.Clear();
		for(int i=0;i<stateList.Length;i++)
		{
			stateIndex[stateList[i].stateName] = stateList[i];
		}
	}

	void Start()
	{

	}
	
	void FixedUpdate()
	{

		if(delayFrames == 0)
		{
            if(customInput != null)
            {
                customInput();
            }

			curState.UpdateState();

            if(pushForce != Vector3.zero)
            {
                RecoverFromFreeze();
                rigidbodyRef.AddForce(pushForce, ForceMode.Impulse);
                pushForce = Vector3.zero;
            }

            curFrameTime += Time.fixedDeltaTime;

			if(curFrameTime >= curState.duration)
			{
				curState.EndState();
			}
		}
		else
		{ 

			if(shaking)
				ShakingOnDelay();

			if(currentAnimationState != null)
            {
				currentAnimationState.time = freezedAnimationTime;
            }

			delayFrames--;

            if(pushFrames != -1)
            {
                if(delayFrames <= pushFrames)
                {
                    if(pushForce != Vector3.zero)
                    {
                        RecoverFromFreeze();
                        rigidbodyRef.AddForce(pushForce, ForceMode.Impulse);
                        pushForce = Vector3.zero;
                    }
                }
            }
			if(delayFrames <= 0)
			{
                RecoverFromFreeze();
			}
		}
	}

    void RecoverFromFreeze()
    {
        currentAnimationState.speed = freezedAnimationSpeed;

        if(rigidbody.constraints == RigidbodyConstraints.FreezeAll)
        {
            rigidbodyRef.constraints = RigidbodyConstraints.FreezePositionY |
                RigidbodyConstraints.FreezeRotationX |
                    RigidbodyConstraints.FreezeRotationZ;
        }
        modelTransformRef.localPosition = Vector2.zero;
    }


    void Update()
    {
        if(delayFrames != 0)
        {
            if(currentAnimationState != null)
            {
				currentAnimationState.time = freezedAnimationTime;
            }
		}
	}

	protected virtual void CustomUpdate()
	{

	}

	void ShakingOnDelay()
	{
		GameObject camera = GameObject.FindWithTag("MainCamera");
		Vector3 camFor = camera.transform.forward;
		Vector3 shakeNorm = new Vector3(camFor.z, 0 , -camFor.x).normalized; // horizontal shakiness
		float phase = 0;
		float shakedTime = delayFrames * Time.fixedDeltaTime; 

		phase = Mathf.Cos(shakedTime*shakeFrequency * Mathf.PI * 2);
		modelTransformRef.localPosition = shakeNorm * shakeDistance * phase; 
	}
	
	public void ToState(string nextState)
	{
		if(!stateIndex.ContainsKey(nextState))
		{
			Debug.LogError(gameObject.ToString() + " undefined state: " + nextState);
			return;
		}

		if(curState != null)
		{
			MonitersOnExit(curState.stateName);
			curState.Exit();
		}

		curState = stateIndex[nextState];
		curFrameTime = 0;

		curState.Enter();
		MonitersOnEnter(curState.stateName);

	}

    private int pushFrames = -1;
    public void DelayFrame(int frames, bool shake, float distance = 0, float frequency = 0,int pushFrames = -1)
	{
		if(frames > 0)
		{
			delayFrames = frames;
            this.pushFrames = pushFrames;
			shaking = shake;
			if(shaking)
			{
				shakeDistance = distance;
				shakeFrequency = frequency;
			}
            rigidbodyRef.constraints = RigidbodyConstraints.FreezeAll;

            if(currentAnimationState != null)
			{
				freezedAnimationTime = currentAnimationState.time;
				freezedAnimationSpeed = currentAnimationState.speed;
				currentAnimationState.speed = 0;
			}
		}
        else{
            this.pushFrames = -1;
        }
	}
 
	protected bool LoseHP(int hpLost)
	{
		hpLost = Mathf.Clamp(hpLost,0,hpLost);
		property.hp.Value -= hpLost;
		if(property.hp <= 0)
		{
			Die();
			return false;
		}
		else
		{
			return true;
		}
	}

	
	protected bool LoseDP(int dpLost)
	{
		dpLost = Mathf.Clamp(dpLost,0,dpLost);
		property.dp.Value -= dpLost;
		if(property.dp <= 0)
		{
			return false;
		}
		else
		{
			return true;
		}
	}


	public virtual void ReceiveDamage (Damaged obj)
	{
		InjuryInformation injuryInfo = obj.injury;

		if(injuryInfo.push != Vector3.zero)
		{
			Vector3 lookAtDirection = -injuryInfo.push;
			lookAtDirection.y = 0;
			transformRef.LookAt(transformRef.position + lookAtDirection);
			targetRotation = transform.rotation.eulerAngles.y;
		}
		else
		{
			if(bodyState != BodyState.Armor)
			{
				transformRef.LookAt(obj.sender.transformRef.position);
				transformRef.rotation = Quaternion.Euler(new Vector3(0, transformRef.rotation.eulerAngles.y, 0));
				targetRotation = transformRef.rotation.eulerAngles.y;
			}
		}

		if (null != bodyRenderer) {
			bodyRenderer.material.SetFloat("_TexOffset", 0.0f);
			StartCoroutine(CoRecoverNormal());
		}

		if (null != bloodEffect) {
			bloodEffect.Play(obj.character.transform.position, Quaternion.Euler(0, 0, 0));
		}
		//Debug.Log(this + " Got Damaged To Injured State" + injuryInfo.injury + "property: " + property);

		switch(bodyState)
		{
		case BodyState.Evading: 
		case BodyState.Normal:
		{
			if(LoseHP(injuryInfo.injury))
			{
                if(InGameSetting.vibration)
                {
                    Handheld.Vibrate();
                }

				if(injuryInfo.fall == false)
				{
					ToState(injuredState.stateName);
				}
				else
				{
					ToState(fallState.stateName);
				}

                pushForce += injuryInfo.push;
				
                if(slashSign != null)
                {
                    if(CanBeSlash())
                    {
                        slashSign.gameObject.SetActive(true);
                    }
                }
			}
		}
		break;
		case BodyState.Defending:
		{
			if(LoseDP(injuryInfo.breakDefense))
			{
				ToState(defendState.stateName);
			}
			else
			{
				ToState(defenseBrokenState.stateName);
			}

			pushForce += injuryInfo.push;
		}
			break;
		case BodyState.Armor:
		{
			LoseHP(injuryInfo.injury);
		}
			break;
		case BodyState.Inactive:
			// shouldn't get hit when overwhelming
			break;
		}

        HitDelayAndShakeEffect effect = HitDelayAndShakeEffect.instance;
        int delayFrames = injuryInfo.receiverDelayFrames < 0? effect.defaultDelayFrames: injuryInfo.receiverDelayFrames;
        int pushFrames = injuryInfo.receiverPushFrames < 0?-1:injuryInfo.receiverPushFrames;
        DelayFrame(delayFrames, true, effect.defaultShakeDistance, effect.defaultShakeFrequency,pushFrames);
    }
        
	protected IEnumerator CoRecoverNormal() {
		float currentOffset = bodyRenderer.material.GetFloat("_TexOffset");
		float recoverTotalTime = 0.1f;
		float recoverInterval = 0.02f;
        bodyRenderer.material.SetFloat("_TexOffset", 0);
		float loopTime = recoverTotalTime / recoverInterval;
		float change = (1.0f - currentOffset) / loopTime;

		for(int ind = 1; ind <= loopTime; ++ind) {
			currentOffset += change;
			if (currentOffset > 1.0f) {
				currentOffset = 1.0f;
			}
			bodyRenderer.material.SetFloat("_TexOffset", currentOffset);
            yield return new WaitForSeconds(recoverInterval);
		}
	}

    void Die(){
		Death message = new Death();
		message.character = this;
		onDeath.SendEventMessage(message);
		ToState(deadState.stateName);
		bodyState = BodyState.Inactive;
	}
	
	void SendDamage (Hitting obj)
	{
        InjuryInformation injury = obj.injury;
        if(injury.slash)//slash case 
        {
            ToState(slashState.stateName);
            return;
        }
        if(delayFrames == 0)//fix bug when hitting multiple enemies cause pause
        {    		
    		HitDelayAndShakeEffect effect = HitDelayAndShakeEffect.instance;
            int delayFrame = injury.senderDelayFrames < 0? effect.defaultDelayFrames: injury.senderDelayFrames;
            DelayFrame(delayFrame, false);
        }
    }

	//set character's property to frameAction
	void SetPropertyData()
	{

	}

	#region events 
	public GameEvent<StateUpdate> onStateUpdate = new GameEvent<StateUpdate>();
	public class StateUpdate
	{
		public Character character;
	}

	public GameEvent<FrameUpdate> onFrameUpdate = new GameEvent<FrameUpdate>();
	public class FrameUpdate
	{
		public Character character;
	}
	
	public GameEvent<DirectionCommand> onDirectionCommand = new GameEvent<DirectionCommand>();
	public class DirectionCommand
	{
		public Character character;
		public float direction;
		public string newStateName = "";
	}
	
	public GameEvent<DirectionCommandEnd> onDirectionCommandEnd = new GameEvent<DirectionCommandEnd>();
	public class DirectionCommandEnd
	{
		public Character character;
		public float direction;
		public string newStateName = "";
	}
	
	public GameEvent<ButtonCommand> onButtonCommand = new GameEvent<ButtonCommand>();
	public class ButtonCommand
	{
		public Character character;
		public ButtonType button;
	}

	
	public GameEvent<ButtonCommandEnd> onButtonCommandEnd = new GameEvent<ButtonCommandEnd>();
	public class ButtonCommandEnd
	{
		public Character character;
		public ButtonType button;
	}
 
	public GameEvent<Hitting> onHitting = new GameEvent<Hitting>();
	public class Hitting
	{
		public Character character;
		public Character receiver;
		public InjuryInformation injury;
	}

	
	public GameEvent<Damaged> onDamaged = new GameEvent<Damaged>();
	public class Damaged
	{
		public Character character;
		public Character sender;
		public InjuryInformation injury;
	}

	
	public GameEvent<Death> onDeath = new GameEvent<Death>();
	public class Death
	{
		public Character character;
	}

	public GameEvent<HitGround> onHitGround = new GameEvent<HitGround>();
	public class HitGround
	{
		public Character character;
	}


	#endregion

	#region AI interface region
	public void OnAICommandMove(Vector3 dest,string newStateName = null)
	{
		Camera cam = Camera.main;
		Vector3 dir = dest - transform.position;
		dir.Normalize();
		float angle = Mathf.Atan2( dir.x,dir.z);
		float directionIndegree = cam.transform.rotation.y + angle* Mathf.Rad2Deg;
		DirectionCommand message = new DirectionCommand();
		message.character = this;
		message.direction = directionIndegree;
		message.newStateName = newStateName;
		onDirectionCommand.SendEventMessage(message);
	}

	public void OnAICommandMoveEnd(Vector3 dest,string newStateName = null)
	{
		Camera cam = Camera.main;
		Vector3 dir = dest - transform.position;
		dir.Normalize();
		float angle = Mathf.Atan2( dir.z,dir.y );
		if(cam == null) return;
		float directionIndegree = cam.transform.rotation.y + angle* Mathf.Rad2Deg;
		DirectionCommandEnd message = new DirectionCommandEnd();
		message.character = this;
		message.direction = directionIndegree;
		message.newStateName = newStateName;
		onDirectionCommandEnd.SendEventMessage(message);
	}
	#endregion

	#region AI state moniter
	public List<IStateMoniter> moniters = new List<IStateMoniter>();

	public void RegisterStateMoniter(IStateMoniter moniter)
	{
		moniters.Add(moniter);
	}

	void MonitersOnEnter(string state){
        for(int i=0;i<moniters.Count;i++)
        {
            moniters[i].OnStateEnter(state);
        }
	}

	void MonitersOnExit(string state){
		for(int i=0;i<moniters.Count;i++)
		{
			moniters[i].OnStateExit(state);
		}
	}
	#endregion

	void OnDestroy(){
		moniters.Clear();
	}


	#region Ground Collison

	void OnCollisionEnter(Collision collision)
	{
		if(collision.collider.CompareTag("ground"))
		{
			HitGround message = new HitGround();
			message.character = this;
			onHitGround.SendEventMessage(message);

            /*
            rigidbodyRef.constraints = RigidbodyConstraints.FreezePositionY | 
                RigidbodyConstraints.FreezeRotationX | 
                    RigidbodyConstraints.FreezeRotationZ;
                    */
		}
	}

	#endregion
    //for lock target
    public void FaceToTarget()
    {
        if(LockTarget != null)
        {
            Vector3 dest = new Vector3(lockTarget.transform.position.x,transform.position.y,lockTarget.transform.position.z);
            FaceToPosition(dest);
        }
    }

    public void FaceToPosition(Vector3 dest)
    {
        Camera cam = Camera.main;
        Vector3 dir = dest - transform.position;
        dir.Normalize();
        FaceToDir(dir);
    }

    public void FaceToDir(Vector3 dir)
    {
        Camera cam = Camera.main;
        float angle = Mathf.Atan2( dir.x,dir.z);        
        float directionIndegree = cam.transform.rotation.y + angle* Mathf.Rad2Deg;
        FaceTotTargetRotation(directionIndegree);
    }

    public void FaceTotTargetRotation(float directionIndegree)
    {
        targetRotation = directionIndegree; 
        transform.rotation = Quaternion.Euler(new Vector3(0, targetRotation, 0));
    }
                           

    //use this to force to set null target
    public void SetNullTarget()
    {
        lockTarget =  null;
    }

    public void ActivateFlashEffect(bool activate)
    {
        if (weaponFlashObj) {
            weaponFlashObj.SetActive (activate);
        }
    }

    public bool CanBeSlash()
    {
        if((float)(property.hp)/(float)(property.hpMax) < slashRatio)
        {
            return true;
        }
        return false;
    }

}

